package dsproject;

import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Iterator;

public class Node implements Serializable {

    private int id;
    private int clock;
    private float x, y;
    private STATUS clStatus;
    private Node isBelongsTo;
    private HashMap<ResourceState, HashSet<Sending>> resourceList;
    private HashMap<ResourceState.TYPE, Integer> currentState;

    //Try to hide the fact the node is an object instead of a Tread/Computer with its own environment. Updated by initialize() call
    public enum STATUS {

        HEAD, CLAIMED
    }

    public Node() {
        this.x = 0;
        this.y = 0;
        this.clStatus = STATUS.CLAIMED;
        this.resourceList = new HashMap<ResourceState, HashSet<Sending>>();
        this.isBelongsTo = null;
        this.currentState = new HashMap<ResourceState.TYPE, Integer>();
    }

    public void rcvPacket(Message msg, int currentTime, Simulator.TYPE type, Node sender) {
        boolean foundTarget = false;

        // Initialize the node attributes
        intialize(currentTime);

        switch (type) {
            case CLASSIC:
                bClassic(msg, sender);
                break;
            case CACHE:
                bCache(msg, sender);
                break;
            case CACHEv2:
                bCachev2(msg, sender);
                break;// Undone yet TODO
            case CACHEvH:
                bCachevH(msg, sender);
                break;// Undone yet TODO
            default:
                System.out.println("Undefined behaviour for simulation type: " + type.toString());
        }

        //increment the sequence id of the message
        msg.incSeqId();

        //Return whether the target is found or not
        if (foundTarget) {
            msg.getRequest().notifyFound();
        }
    }

    private void bClassic(Message msg, Node sender) {

        Node destination = msg.getBody().iterator().next().getOwner();
        if (msg.getType() == Message.TYPE.NORMAL) {
            if (!msg.getRequest().isAnswered()) {
                if (destination.getBelong().equals(this)) {
                    if (!answerUsingRSList(msg) && !answerAsDestination(msg)) {
                        System.out.println("CH unable to answer the message" + clock);
                    }
                    msg.getRequest().notifyFound();
                }
            }
        } else {
            if (answerAsDestination(msg)) {
                msg.getRequest().notifyFound();
            }
            else{
                for (ResourceState resStateMsg : msg.getBody()) {
                    updateRS(resStateMsg, sender);
                }
            }
        }
    }

    private void bCache(Message msg, Node sender) {

        // 1.We add and update the missing resource state from the Msg in the resource state of the current node
        for (ResourceState resStateMsg : msg.getContent()) {
            updateRS(resStateMsg, sender);
        }

//Clean old TTL sent in the message and contained in the node, add rs of the current node and put it in the answer.
        this.ttlCheck();

        for (Entry<ResourceState.TYPE, Integer> rs : currentState.entrySet()) {
            updateRS(new ResourceState(this, rs.getKey(), rs.getValue(), clock), sender);
        }

//Piggyback all our data in the message to forward
        msg.setContent(new HashSet(resourceList.keySet()));

        // 2. Answer the message if we have the capability
        if (!msg.getRequest().isAnswered()) {
            if (answerAsDestination(msg)) {
                msg.getRequest().notifyFound();
            } else {
                if (answerUsingRSList(msg)) {
                    msg.getRequest().notifyFound();
                }
            }
        }
    }

    /**

     * @param msg
     */
    private void bCachev2(Message msg, Node sender) {

        //TO DO : CHECK NULL POINTER EXCEPTION
        HashSet content = new HashSet();

        // 1.We add and update the missing resource state from the Msg in the resource state of the current node
        for (ResourceState resStateMsg : msg.getContent()) {
            updateRS(resStateMsg, sender);
        }

// Clean old TTL sent in the message and contained in the node, add rs of the current node and put it in the answer.
        this.ttlCheck();

        for (Entry<ResourceState.TYPE, Integer> rs : currentState.entrySet()) {
            updateRS(new ResourceState(this, rs.getKey(), rs.getValue(), clock), this);
        }

// Fill the answer if the Node have the capability, and notify the request
        if (!msg.getRequest().isAnswered()) {
            if (answerAsDestination(msg)) {
                msg.getRequest().notifyFound();
            } else {
                if (answerUsingRSList(msg)) {
                    msg.getRequest().notifyFound();
                }

            }
        }
        // Piggyback selected data in the message to forward
        for (Entry<ResourceState, HashSet<Sending>> entry : this.resourceList.entrySet()) {
            if (!entry.getValue().contains(new Sending(msg.getNext()))) {
                entry.getValue().add(new Sending(msg.getNext(), entry.getKey().getTTL()));
                content.add(entry.getKey());
            }

        }
        msg.setContent(content);
        //  System.out.println(content);
    }

    /**
     * Try to handle hierarchy, by using the following mecanism:
     * cluster heads => its claimed node : piggyback the cluster's data, according to previous sending
     * claimed nodes => its cluster head : piggyback my data
     * otherwise nothing
     * @param msg
     */
    private void bCachevH(Message msg, Node sender) {

        HashSet content = new HashSet();

        // 1.We add and update the missing resource state from the Msg in the resource state of the current node
        for (ResourceState resStateMsg : msg.getContent()) {
            updateRS(resStateMsg, sender);
        }

// Clean old TTL sent in the message and contained in the node, add rs of the current node and put it in the answer.
        this.ttlCheck();

        for (Entry<ResourceState.TYPE, Integer> rs : currentState.entrySet()) {
            updateRS(new ResourceState(this, rs.getKey(), rs.getValue(), clock), this);
        }

// Fill the answer if the Node have the capability, and notify the request
        if (!msg.getRequest().isAnswered()) {
            if (answerAsDestination(msg)) {
                msg.getRequest().notifyFound();
            } else {
                if (answerUsingRSList(msg)) {
                    msg.getRequest().notifyFound();
                }

            }
        }

        if (msg.getNext() != null) {

            // Piggyback selected data in the message to forward
            if (this.clStatus == clStatus.HEAD && msg.getNext().getBelong().equals(this)) {
                for (Entry<ResourceState, HashSet<Sending>> entry : this.resourceList.entrySet()) {
                    if (entry.getKey().getOwner().equals(this) && !entry.getValue().contains(new Sending(msg.getNext()))) {
                        entry.getValue().add(new Sending(msg.getNext(), entry.getKey().getTTL()));
                        content.add(entry.getKey());
                    }

                }
            } else {
                if (this.clStatus == clStatus.CLAIMED && msg.getNext().getBelong().equals(this) && msg.getNext().getClStatus() == clStatus.HEAD) {
                    for (Entry<ResourceState.TYPE, Integer> entry : currentState.entrySet()) {
                        content.add(new ResourceState(this, entry.getKey(), entry.getValue(), clock));
                    }

                }
            }
        }
        msg.setContent(content);
    }

//Generate Fictive Resources States of this node
    public void intialize(int currentTime) {
        this.clock = currentTime;
        //Generate Fictive Resources States of this node
        currentState.clear();
        for (ResourceState.TYPE type : ResourceState.TYPE.values()) {
            currentState.put(type, ResourceState.generateFictiveData(type));
        }

    }

    public void ttlCheck() {

        Iterator<Entry<ResourceState, HashSet<Sending>>> entitr = this.resourceList.entrySet().iterator();
        Iterator<Sending> sditr;
        Entry<ResourceState, HashSet<Sending>> entry;
        while (entitr.hasNext()) {
            entry = entitr.next();
            if (entry.getKey().getTTL() < clock) {
                entitr.remove();
            } else {
                sditr = entry.getValue().iterator();
                while (sditr.hasNext()) {
                    if (!sditr.next().isValid(clock)) {
                        sditr.remove();
                    }

                }
            }
        }
    }

    // The HashMap could be the wrong container, but I didn't find the good container
    private void updateRS(ResourceState newRS, Node sender) {
        Boolean foundRS = false;
        Sending sd = new Sending(sender, newRS.getTTL());

        // We are forced to iterate over the hashMap, because we are not able to get the entry associated with a key
        // this.resourceList.getEntry(newRS) is private
        for (Entry<ResourceState, HashSet<Sending>> entry : this.resourceList.entrySet()) {
            if (newRS.equals(entry.getKey())) {
                foundRS = true;
                if (entry.getKey().getTTL() < newRS.getTTL()) {
                    //update the currentNode
                    entry.getKey().setTTL(newRS.getTTL());
                    entry.getKey().setData(newRS.getData());
                    entry.getValue().remove(sd);
                    entry.getValue().add(sd);
                    break;

                }


            }
        }

        // we didn't find this resourcestate (same id AND same type), so we add it
        if (!foundRS) {
            HashSet<Sending> temp = new HashSet<Sending>();
            temp.add(sd);
            resourceList.put(newRS, temp);
        }

    }

    /**
     * Try to answer the message as the destination node
     * Fail if we are not the destination node
     * @param msg
     * @return true if we are the destination node and then we have fill the answer of the message
     */
    private boolean answerAsDestination(Message msg) {
        Node destination = msg.getBody().iterator().next().getOwner();
        if (destination.equals(this)) {
            //For each RS requested, sent our information,
            for (ResourceState rs : msg.getBody()) {
                rs.setData(currentState.get(rs.getType()));
            }

            return true;
        }

        return false;
    }

    /**
     * Answer the message using our RSList
     * @param msg
     * @return true if this node have all the asked RS
     */
    private boolean answerUsingRSList(Message msg) {

        boolean foundAll = false;
        HashSet<ResourceState> answer = new HashSet<ResourceState>();

        for (ResourceState askedrs : msg.getBody()) {
            foundAll = false;
            for (ResourceState myrs : this.resourceList.keySet()) {
                if (askedrs.equals(myrs)) {//If we have the requested RS
                    answer.add(myrs);
                    foundAll =
                            true;
                    break;

                }


            }
            if (!foundAll) {
                break;
            }

        }
        if (foundAll) {//If every RS requested are found, then set the answer in the message
            msg.setBody(answer);
            return true;
        } else {
            return false;
        }

    }

    public STATUS getClStatus() {
        return clStatus;
    }

    public void setClStatus(STATUS clStatus) {
        this.clStatus = clStatus;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public float getX() {
        return x;
    }

    public void setX(float x) {
        this.x = x;
    }

    public float getY() {
        return y;
    }

    public void setY(float y) {
        this.y = y;
    }

    @Override
    public String toString() {
        return this.getId() + "(" + this.getX() + ',' + this.getY() + ")";
    }

    public void setBelong(Node isBelong) {
        this.isBelongsTo = isBelong;
    }

    public Node getBelong() {
        return this.isBelongsTo;
    }
}


